#!/usr/bin/perl

# Copyright (c) 2012, 2021, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is also distributed with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms,
# as designated in a particular file or component or in included license
# documentation.  The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have included with MySQL.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# -*- cperl -*-
#
# MySQL Cluster compile script to bridge the gap between
# different build systems in different versions of MySQL Server
#
# This script is intended for internal use
#
use strict;
use Cwd 'abs_path';
use File::Basename;
use Getopt::Long;

# Automatically flush STDOUT
select(STDOUT);
$| = 1;

# Only add the command line options handled by this script, 
# thus acting like a filter and passing all other arguments
# straight through
my $opt_debug;
my $opt_build_type;
my $opt_build = 1;
my $opt_just_print;
my $opt_vanilla;
my $opt_autotest;
my $opt_valgrind;
my $opt_distcheck;
my $opt_parse_log;
Getopt::Long::Configure("pass_through");
GetOptions(

  # Build MySQL Server and NDB with debug
  'debug!' => \$opt_debug,
  'with-debug:s' => sub { $opt_debug = 1; },
  'build-type=s' => \$opt_build_type,
  'build!' => \$opt_build,
  'c|just-configure' => sub { $opt_build = 0; },
  'n|just-print' => \$opt_just_print,
  'vanilla' => \$opt_vanilla,
  'autotest' => \$opt_autotest,
  'valgrind' => \$opt_valgrind,
  'distcheck' => \$opt_distcheck,

  # Special switch --parse-log=<file> which reads a log file (from build) and
  # parses it for warnings
  'parse-log=s' => \$opt_parse_log,
) or exit(1);

# Find source root directory, assume this script is
# in <srcroot>/storage/ndb/
my $opt_srcdir = dirname(dirname(dirname(abs_path($0))));
die unless -d $opt_srcdir; # Sanity check that the srcdir exist
if ($^O eq "cygwin") {
  # Convert posix path to Windows mixed path since cmake
  # is most likely a windows binary
  $opt_srcdir= `cygpath -m $opt_srcdir`;
  chomp $opt_srcdir;
}

# Parse given log file for warnings
if ($opt_parse_log)
{
  use IO::File;
  my $file = IO::File->new($opt_parse_log, 'r')
      or die "Failed to open file $opt_parse_log: $!";
  my $parser = WarningParser->new(srcdir => $opt_srcdir,
				  unified => 1,
                                  verbose => 1);
  while (my $line = <$file>)
  {
    $parser->parse_line($line);
  }
  $parser->report($0);
  exit(0);
}

# Check that cmake exists and figure out it's version
my $cmake_version_id;
{
  my $version_text = `cmake --version`;
  print $version_text;
  die "Could not find cmake" if ($?);
  if ( $version_text =~ /^cmake version ([0-9]*)\.([0-9]*)\.*([^\s]*)/ )
  {
    #print "1: $1 2: $2 3: $3\n";
    $cmake_version_id= $1*10000 + $2*100 + $3;
    #print "cmake_version_id: $cmake_version_id\n";
  }
  die "Could not parse cmake version" unless ($cmake_version_id);
}

# Replace gcc with g++ in CXX environment variable
if(defined $ENV{"CXX"} and $ENV{"CXX"} =~ m/gcc/)
{
  my $old_cxx= $ENV{"CXX"};
  $ENV{"CXX"} =~ s/gcc/g++/;
  print("compile-cluster: switched CXX compiler from '$old_cxx' to '$ENV{CXX}'\n");
}

# Remove -fno-exceptions from CXXFLAGS environment variable
if(defined $ENV{"CXXFLAGS"} and $ENV{"CXXFLAGS"} =~ "-fno-exceptions")
{
  $ENV{"CXXFLAGS"} =~ s/-fno-exceptions//g;
  print("compile-cluster: stripped off -fno-exceptions from CXXFLAGS='$ENV{CXXFLAGS}'\n");
}

#
# Configure
#
{
  # Remove old CMakeCache.txt(ignore if not exists) to
  # force fresh configure
  unlink("CMakeCache.txt");

  my @args;
  if ($opt_debug)
  {
    print("compile-cluster: debug build requested\n");
    push(@args, "-DWITH_DEBUG=1");
    push(@args, "-DMYSQL_MAINTAINER_MODE=0");
  }

  if ($opt_vanilla)
  {
    # Use default options for building
    print("compile-cluster: vanilla build requested, no sugar\n");

    # Turn off automatic detection of WITH_NDBCLUSTER
    # in MySQL Cluster version(i.e when version string ends in -ndb-Y.Y.Y)
    push(@args, "-DWITH_NDB_DEFAULT_PLUGIN_DETECT=0");
  }
  else
  {
    # Hardcoded options controlling how to build MySQL Server
    push(@args, "-DWITH_SSL=bundled"); # Consistent error messages

    # Hardcoded options controlling how to build NDB
    push(@args, "-DWITH_PLUGIN_NDBCLUSTER=1");
    push(@args, "-DWITH_NDB_TEST=1");
    push(@args, "-DWITH_NDBAPI_EXAMPLES=1")
      unless ($^O eq "cygwin" or $^O eq "MSWin32")
  }

  if ($opt_autotest)
  {
    print("compile-cluster: autotest build requested, extra everything\n");
    push(@args, "-DWITH_NDB_CCFLAGS='-DERROR_INSERT'");
    push(@args, "-DWITH_EMBEDDED_SERVER=1");
  }

  if ($opt_valgrind)
  {
    print("compile-cluster: valgrind build requested, adjusting the knobs\n");
    # Add HAVE_purify to compiler flags in order to silence
    # inspected warnings
    push(@args, "-DCMAKE_C_FLAGS='-DHAVE_purify'");
    push(@args, "-DCMAKE_CXX_FLAGS='-DHAVE_purify'");
    # Turn on use of the valgrind headers to enhance detection
    push(@args, "-DWITH_VALGRIND=1");
  }

  # The cmake generator to use
  if ($opt_build_type)
  {
    push(@args, "-G \"$opt_build_type\"");
  }

  # Sets installation directory,  bindir, libdir, libexecdir etc.
  # The equivalent CMake variables are given without prefix
  # e.g if --prefix is /usr and --bindir is /usr/bin
  # then cmake variable (INSTALL_BINDIR) must be just "bin"
  my $opt_prefix;
  sub set_installdir
  {
    my($path, $varname) = @_;
    my $prefix_length = length($opt_prefix);
    if (($prefix_length > 0) && (index($path,$opt_prefix) == 0))
    {
      # path is under the prefix, remove the prefix and
      # maybe following "/"
      $path = substr($path, $prefix_length);
      if(length($path) > 0)
      {
        my $char = substr($path, 0, 1);
        if($char eq "/")
        {
          $path= substr($path, 1);
        }
      }
      if(length($path) > 0)
      {
        push(@args, "-D$varname=$path");
      }
    }
  }

  # Process --configure style arguments which need special conversion 
  my $opt_bindir;
  my $opt_libdir;
  my $opt_libexecdir;
  my $opt_includedir;
  my $opt_with_zlib_dir;
  my $opt_with_ssl;
  my $opt_localstatedir;
  my $opt_mysql_maintainer_mode;
  my $opt_with_gcov;
  my $opt_with_comment;
  my $opt_with_plugins;
  my $opt_without_plugin;
  my $opt_extra_charsets;
  my $opt_with_extra_charsets;  
  Getopt::Long::Configure("pass_through");
  GetOptions(
    'prefix=s' => \$opt_prefix,
    'srcdir=s' => \$opt_srcdir,
    'bindir=s' => \$opt_bindir,
    'libdir=s' => \$opt_libdir,
    'libexecdir=s' => \$opt_libexecdir,
    'includedir=s' => \$opt_includedir,
    'with-zlib-dir=s' => \$opt_with_zlib_dir,
    'with-ssl:s' => \$opt_with_ssl,
    'localstatedir=s' => \$opt_localstatedir,
    'mysql-maintainer-mode=s' => \$opt_mysql_maintainer_mode,
    'with-gcov' => \$opt_with_gcov,
    'with-comment=s' => \$opt_with_comment,
    'with-plugins=s' => \$opt_with_plugins,
    'without-plugin=s' => \$opt_without_plugin,
    'with-extra-charsets=s' => \$opt_with_extra_charsets,
    'extra-charsets=s' => \$opt_extra_charsets,
  ) or exit(1);

  if($opt_prefix)
  {
    push(@args, "-DCMAKE_INSTALL_PREFIX=$opt_prefix");
  }
  if($opt_bindir)
  {
    set_installdir($opt_bindir, "INSTALL_BINDIR");
  }
  if($opt_libdir)
  {
    set_installdir($opt_libdir, "INSTALL_LIBDIR");
  }
  if($opt_libexecdir)
  {
    set_installdir($opt_libexecdir, "INSTALL_SBINDIR");
  }
  if($opt_includedir)
  {
    set_installdir($opt_includedir, "INSTALL_INCLUDEDIR");
  }
  if($opt_with_zlib_dir)
  {
    $opt_with_zlib_dir = "system"
      if ($opt_with_zlib_dir ne "bundled");
    push(@args, "-DWITH_ZLIB=$opt_with_zlib_dir");
  }
  if($opt_with_ssl)
  {
    push(@args, "-DWITH_SSL=".($opt_with_ssl ? "yes" : "bundled"));
  }
  if ($opt_localstatedir)
  {
    push(@args, "-DMYSQL_DATADIR=$opt_localstatedir"); 
  }
  if ($opt_mysql_maintainer_mode)
  {
    push(@args, "-DMYSQL_MAINTAINER_MODE=" .
                 ($opt_mysql_maintainer_mode =~ /enable/ ? "1" : "0"));
  }
  if ($opt_with_gcov)
  {
    push(@args, "-DENABLE_GCOV=ON"); 
  }
  if ($opt_with_comment)
  {
    push(@args, "\"-DWITH_COMMENT=$opt_with_comment\""); 
  }
  if($opt_with_plugins)
  {
    my @plugins= split(/,/, $opt_with_plugins);
    foreach my $p (@plugins)
    {
      $p =~ s/-/_/g;
      push(@args, "-DWITH_".uc($p)."=1");
    }
  }
  if($opt_without_plugin)
  {
    push(@args, "-DWITHOUT_".uc($opt_without_plugin)."=1");
  }
  if ($opt_extra_charsets)
  {
    push(@args, "-DWITH_CHARSETS=$opt_extra_charsets"); 
  }
  if($opt_with_extra_charsets)
  {
    push(@args, "-DWITH_EXTRA_CHARSETS=$opt_with_extra_charsets");
  }
  

  # Default conversion of remaining args in ARGV from
  # 1) --arg          -> -DARG=1
  # 2) --arg=value    -> -DARG=value
  # 3) arg=value      -> environment variable arg=value
  foreach my $option (@ARGV)
  {
    if ($option =~  /^--/)
    {  
      # Remove leading --
      $option = substr($option, 2);
    
      my @v  = split('=', $option);
      my $name = shift(@v);
      $name = uc($name);
      $name =~ s/-/_/g;
      if (@v)
      {
        push(@args, "-D$name=".join('=', @v));
      }
      else
      {
        push(@args, "-D$name=1");
      }
    }
    else
    {
 
      # This must be environment variable
      my @v  = split('=', $option);
      my $name = shift(@v);
      if(@v)
      {
        $ENV{$name} = join('=', @v);  
      }
      else
      {
        die "unhandled argument '$option' found"; 
      }
    }
  }
  
  # The source directory to build from
  die "srcdir already contains CMakeCache.txt, this will not work!"
    if (-f "$opt_srcdir/CMakeCache.txt");
  push(@args, $opt_srcdir);
  
  cmd("cmake", @args);
}

#
# Build
#
if (!$opt_build)
{
  print "Configuration completed, skipping build(used --no-build)\n";
  exit(0);
}

{
  if ($cmake_version_id >= 20800)
  {
    # Use the universal "cmake --build <dir>" way of building
    # which is available from cmake 2.8 and works on all platforms
    my @args;
    push(@args, "--build");
    push(@args, ".");

    if ($^O eq "cygwin" or $^O eq "MSWin32")
    {
      # Choose to build RelWitDebInfo by default on Windows
      my $config = 'RelWithDebInfo';
      if ($opt_debug)
      {
	$config = 'Debug';
      }
      push(@args, "--config");
      push(@args, $config);
    }

    build_cmd("cmake", @args);

  }
  else
  {
    # Use make to build, not supported on Windows
    die "You need to install cmake with version > 2.8"
      if ($^O eq "cygwin" or $^O eq "MSWin32");

    build_cmd("make");
  }
}

if ($opt_distcheck)
{
    print "\n";
    print "NOTE! 'make distcheck' not (yet) supported in this version\n";
    print "\n";
}

exit(0);


sub cmd {
  my ($cmd, @a)= @_;
  my $cmd_str = join(' ', $cmd, @a);
  print "compile-cluster: '$cmd_str'\n";
  return if ($opt_just_print);
  system($cmd, @a)
    and print("command '$cmd_str' failed\n")
	  and exit(1);
}

use IPC::Open2;
sub build_cmd {
  my ($cmd, @args) = @_;
  my $cmd_str = join(' ', $cmd, @args);
  print "compile-cluster: '$cmd_str'\n";
  return if ($opt_just_print);
  $cmd_str.= " 2>&1";

  # Create warning parser and pass every ouput line through it
  my $parser = WarningParser->new(srcdir => $opt_srcdir,
                                  unified => 1,
                                  verbose => 1);

  my ($chld_out, $chld_in);
  my $pid = open2($chld_out, $chld_in, $cmd_str) or die $!;

  # Install handler to make sure that build is killed
  # off even in case the perl script dies
  local $SIG{__DIE__} = sub {
      print STDERR "Ooops, script died! Killing the build(pid = $pid)\n";

      # NOTE! Kill with negative signal number means kill process group
      my $ret = kill(-9, $pid);
      print STDERR "  kill(-9, $pid) -> $ret\n";

      # Just checking, should return 0
      $ret = kill(0, $pid);
      print STDERR "  kill(0, $pid) ->  $ret\n";

      # Wait for process to terminate
      print STDERR "  waitpid($pid, 0)\n";
      $ret= waitpid($pid, 0);
      print STDERR "  waitpid returned $ret!\n";
  };

  while (my $line = <$chld_out>)
  {
    if (!$parser->parse_line($line))
    {
      # Warning parser didn't print the line, print it
      print $line;
    }
  }
  waitpid($pid, 0);
  my $exit_status = $?;
  my $exit_code = ($exit_status >> 8);
  print "Build completed with exit_code: $exit_code(status: $exit_status)\n";
  if ($exit_code)
  {
    print("command '$cmd_str' failed: $!\n");
    exit(1);
  }
  $parser->report($0);
}


# Perl class used by WarningParser for keeping
# track of one individual warning
#
package WarningParser::Warning;
use strict;

sub new {
  my ($class, $file, $line, $text, $compiler)= @_;
  my $self= bless {
    FILE => $file,
    LINE => $line,
    TEXT => $text,
    COMPILER => $compiler,
  }, $class;
  return $self;
}

sub file {
  my ($self) = @_;
  return $self->{FILE};
}

sub line {
  my ($self) = @_;
  return $self->{LINE};
}

sub text {
  my ($self) = @_;
  return $self->{TEXT};
}

# Print the warning in verbose format for easier debugging
sub print_verbose {
  my ($self) = @_;

  print "{\n";
  foreach my $key (keys %$self)
  {
    print "  $key => '$self->{$key}'\n";
  }
  print "}\n";
}

# Print the warning in unified format(easy for automated build system to parse)
# emulate gcc
sub print_unified {
  my ($self) = @_;
  my $file = $self->file();
  my $line = $self->line();
  my $text = $self->text();
  print "$file:$line: warning: $text\n";
}

sub suppress {
  my ($self, $message) = @_;
  die if exists $self->{SUPPRESSED}; # Already suppressed
  die unless $message; # No message
  $self->{SUPPRESSED} = $message;
}

sub is_suppressed {
  my ($self) = @_;
  return exists $self->{SUPPRESSED};
}

sub is_cluster_warning {
  my ($self) = @_;
  my $file = $self->{FILE};
  # Have the string ndb in the file name(including
  # directory so everything below storage/ndb is
  # automatically included)
  if ($file =~ /ndb/)
  {
    return 1;
  }
  return 0;
}


package WarningParser;
use strict;
use Cwd 'abs_path';

sub new {
  my $class= shift;
  my %opts= ( @_ );
  my $srcdir = $opts{srcdir} || die "Must supply srcdir";
  my $verbose = $opts{verbose} || 0;
  my $unified = $opts{unified} || 0;
  my $track_dirs = $opts{track_dirs} || 0;

  my $self= bless {
    # empty array of warnings
    WARNINGS => [],

    # print each warning object as they are accumulated
    VERBOSE => $verbose,

    # print warnings in unified format(i.e the format
    # is converted to look like standard gcc). This makes it
    # easy for higher level tools to parse the warnings
    # regardless of compiler.
    UNIFIED => $unified,

    # Need to keep track of current dir since file name in
    # warnings does not include directory(this is normal
    # in makefiles generated by automake)
    TRACK_DIRS => $track_dirs,

    # Location of source
    SRCDIR => $srcdir,

  }, $class;
  return $self;
}

sub new_warning {
  my ($self, $file, $line, $text, $compiler) = @_;
  #print "new_warning>\n";
  #print "file: '$file', line: $line, text: '$text', compiler: '$compiler'\n";
  my $srcdir = $self->{SRCDIR};
  #print "srcdir: $srcdir\n";
  if ($self->{TRACK_DIRS})
  {
    # "file" does not contain directory, add currently
    # tracked dir
    my $dir = $self->{DIR};
    $file= "$dir/$file";
  }
  if (! -e $file)
  {
    # safety, "file" does not always contain full path
    # and thus calling 'abs_path' on a non existing file
    # would make the script die
    print "Hmmpf, creating warning for file without full path!\n";
  }
  else
  {
    # "srcdir" is in abs_path form, convert also "file" to abs_path
    $file = abs_path($file);
  }

  $file =~ s/^$srcdir//; # Remove leading srcdir
  $file =~ s:^\/::; # Remove leading slash

  return WarningParser::Warning->new($file, $line, $text, $compiler);
}

sub parse_warning {
  my ($self, $line) = @_;

  if ($self->{TRACK_DIRS})
  {
    # Track current directory by parsing makes
    # "Entering/Leaving directory" messages
    if ($line =~ /Entering directory \`(.*)\'/)
    {
      my $dir= $1;
      # Push previous dir onto stack before setting new
      push(@{$self->{DIRSTACK}}, $self->{DIR});
      $self->{DIR}= $dir;
    }

    if ($line =~ /Leaving directory \`(.*)\'/)
    {
      # Pop previous dir from stack and set it as current
      my $prevdir= pop(@{$self->{DIRSTACK}});
      $self->{DIR}= $prevdir;
    }
  }

  # cmake and Visual Studio 10(seems to use msbuild)
  if ($line =~ /^(\d+>)?\s*(.*)\((\d+)\): warning ([^ ]*:.*)$/)
  {
    return $self->new_warning($2, $3, $4, "vs10_msbuild");
  }

  # cmake and Visual Studio 9
  if ($line =~ /^(\d+>)?(?:[a-z]:)?([^:()]*)\((\d+)\) : warning ([^ ]*:.*)$/)
  {
    my ($project, $file, $lineno, $text) = ($1, $2, $3, $4);
    return $self->new_warning($file, $lineno, $text, "vs9");
  }

  # cmake and gcc with line number AND column
  if ($line =~ /([^ ]+\.(c|h|cc|cpp|hpp|ic|i|y|l)):([0-9]+):([0-9]+):[ \t]*warning:[ \t]*(.*)$/)
  {
    my ($file, $junk, $lineno, $colno, $text) = ($1, $2, $3, $4, $5);
    return $self->new_warning($file, $lineno, $text, , "gcc_with_col");
  }

  # cmake and gcc
  if ($line =~ /([^ ]+\.(c|h|cc|cpp|hpp|ic|i|y|l)):[ \t]*([0-9]+):[ \t]*warning:[ \t]*(.*)$/)
  {
    return $self->new_warning($1, $3, $4, "gcc");
  }

  return undef;
}

sub suppress_warning {
  my ($self, $w) = @_;

  # Ignore files not owned by cluster team
  if (!$w->is_cluster_warning())
  {
    $w->suppress('Warning in file not owned by cluster team');
    return 1;
  }

  if ($w->file() =~ /memcache\/extra/)
  {
    $w->suppress('Warning in imported memcache code');
    return 1; 
  }

  # List of supressions consisting of one regex for the dir+file name
  # and one for the warning text. The suppression is stored as a
  # list of arrays, where each array contains two precompiled
  # regexes. If both expressions match, the warning is suppressed.
  my @suppressions = (
    # [ qr/<dirname+filename regex>/, qr/<warning regex>/ ],
    [ qr/DbtuxMeta.cpp/, qr/Warray-bounds/ ],
  );

  foreach my $sup ( @suppressions )
  {
    my $file_pat = $sup->[0];
    my $text_pat = $sup->[1];
    if ($w->file() =~ /$file_pat/ and
	$w->text() =~ /$text_pat/)
    {
      $w->suppress("Suppressed by file suppression: '$file_pat, $text_pat'");
      return 1;
    }
  }

  return 0;
}

# Parse a line for warnings and return 1 if warning was
# found(even if it was suppressed)
#
sub parse_line {
  my ($self, $line) = @_;
  $self->{LINES}++;

  # Remove trailing line feed and new line
  $line =~ s/[\r]+$//g;
  $line =~ s/[\n]+$//g;

  my $w = $self->parse_warning($line);
  if (defined $w)
  {
    if (!$self->suppress_warning($w))
    {
      if ($self->{UNIFIED})
      {
        # Print the warning in UNIFIED format
        $w->print_unified();
      }
      else
      {
        # Just echo the line verbatim
	print "\n$line\n";
      }
    }
    # Print the warning object in verbose mode
    $w->print_verbose() if $self->{VERBOSE};

    # Save the warning for final report
    push(@{$self->{WARNINGS}}, $w);

    return 1;
  }

  return 0;
}

sub report {
  my ($self, $prefix) = @_;
  my $lines = $self->{LINES};

  my $warnings = 0;
  my $suppressed= 0;

  foreach my $w (@{$self->{WARNINGS}})
  {
    if ($w->is_suppressed())
    {
      $suppressed++;
    }
    else
    {
      $warnings++;
    }
  }
  my $total = $warnings + $suppressed;
  print "$prefix: $warnings warnings found(suppressed $suppressed of total $total)\n";
}

1;
